home *** CD-ROM | disk | FTP | other *** search
/ Nebula 1 / Nebula One.iso / Financial / Stopwatch2.3 / Source / Controller.m < prev    next >
Text File  |  1995-06-12  |  16KB  |  763 lines

  1. /*
  2.  * Main controller for Stopwatch app.
  3.  *
  4.  * For legal stuff see the file COPYRIGHT
  5.  */
  6. #import <stdio.h>
  7. #import <ansi/string.h>        /* for import feature only (?) */
  8. #import <bsd/sys/param.h>    /* for MAXPATHLEN */
  9. #import <appkit/NXCType.h>
  10. #import "Controller.h"
  11. #import "StopWatch.h"
  12. #import "InfoPanel.h"
  13. #import "ClientInfo.h"
  14. #import "ClientInspector.h"
  15. #import "SessionEditor.h"
  16. #import "AppIconView.h"
  17. #import "createPath.h"
  18. #import "Preferences.h"
  19.  
  20. #define PRIORITY    NX_MODALRESPTHRESHOLD
  21.  
  22. #define ARCHIVE_FILE    "client.data"
  23. #define TEMPLATE_DIR    "Templates"
  24. #define MAXCLIENTLEN    80
  25.  
  26. #define VERSION        2    /* the current file version that gets written */
  27. int FileVersion;        /* the version of the file being read */
  28.  
  29. void
  30. freeAndCopy( char **ptr, const char *str )
  31. {
  32.   if ( *ptr )
  33.     free( *ptr );
  34.  
  35.   *ptr = NXCopyStringBuffer(str);
  36. }
  37.  
  38. const char *
  39. currentDate()
  40. {
  41.   time_t now;
  42.   struct tm *tm;
  43.   static char buf[10];
  44.  
  45.   time(&now);
  46.   tm = localtime(&now);
  47.  
  48.   sprintf( buf, "%02d/%02d/%02d", tm->tm_mon + 1, tm->tm_mday, tm->tm_year );
  49.   return buf;
  50. }
  51.  
  52. const char *
  53. currentTime()
  54. {
  55.   time_t now;
  56.   struct tm *tm;
  57.   static char buf[10];
  58.  
  59.   time(&now);
  60.   tm = localtime(&now);
  61.  
  62.   sprintf( buf, "%02d:%02d", tm->tm_hour, tm->tm_min );
  63.   return buf;
  64. }
  65.  
  66. /*
  67.  * To avoid having to exec /bin/cp.
  68.  */
  69. int
  70. copyFile( const char *src, const char *dst )
  71. {
  72.   FILE *in, *out;
  73.   int count;
  74.   char buf[BUFSIZ];
  75.  
  76.   if ( ! (in = fopen( src, "r" ) ) ) {
  77.     fprintf( stderr, "Can't open `%s' for reading.\n", src );
  78.     return 0;
  79.   }
  80.  
  81.   if ( ! (out = fopen( dst, "w" ) ) ) {
  82.     fprintf( stderr, "Can't open `%s' for writing.\n", dst );
  83.     fclose(in);
  84.     return 0;
  85.   }
  86.  
  87.   while ( (count = fread( buf, sizeof(char), sizeof(buf), in )) > 0 )
  88.     fwrite( buf, sizeof(char), count, out );
  89.  
  90.   fclose(in);
  91.   fclose(out);
  92.   return 1;
  93. }
  94.  
  95. @interface Controller(PRIVATE)
  96. - selectedClient;
  97. - (int)compare:obj1 :obj2;    /* comparison method for SortList */
  98. - initInvoice;
  99. - (void)addSession:(const char *)startDate
  100.           time:(const char *)startTime
  101.       duration:(int)minutes
  102.        description:(const char *)desc;
  103. - (void)checkStartButton;
  104. @end
  105.  
  106.  
  107. @implementation Controller
  108.  
  109. DPSTimedEntryProc 
  110. showElapsedTime(DPSTimedEntry teNum, double now, char *data)
  111. {
  112.   [(id)data showElapsedTime];
  113.   return (void *)NULL;
  114. }
  115.  
  116. - (void) removeTimedEntry
  117. {
  118.   if ( teNum ) {
  119.     DPSRemoveTimedEntry(teNum);
  120.     teNum = 0 ;
  121.   }
  122. }
  123.  
  124. - (void) addTimedEntry
  125. {
  126.   [self removeTimedEntry];    /* in case there is one */
  127.   
  128.   /* Set it up so that the clock updates every minute */
  129.   teNum = DPSAddTimedEntry( (double)60.0, (DPSTimedEntryProc)showElapsedTime,
  130.                 (void *)self, PRIORITY );
  131. }
  132.  
  133. - free
  134. {
  135.   [self removeTimedEntry];
  136.   [stopwatch free];
  137.   [infoPanel free];
  138.   return [super free];
  139. }
  140.  
  141. - awakeFromNib
  142. {
  143.   [window setFrameAutosaveName:"Stopwatch"];
  144.  
  145.   /* Make the browser's font match the startButton (can't do this in IB) */
  146.   [[[browser matrixInColumn:0] prototype] setFont:[startButton font]];
  147.  
  148.   return self;
  149. }
  150.  
  151. - add:sender
  152. {
  153.   [[ClientInspector sharedInstance] add:sender];
  154.   return self;
  155. }
  156.  
  157. - modify:sender
  158. {
  159.   [[ClientInspector sharedInstance] modify:sender];
  160.   return self;
  161. }
  162.  
  163. - delete:sender
  164. {
  165.   [[ClientInspector sharedInstance] delete:sender];
  166.   return self;
  167. }
  168.  
  169. - undelete:sender
  170. {
  171.   [[ClientInspector sharedInstance] undelete:sender];
  172.   return self;
  173. }
  174.  
  175. - (void)enableAdd:(BOOL)flag
  176. {
  177.   [addMenuItem setEnabled:flag];
  178. }
  179.  
  180. - (void)enableModify:(BOOL)flag
  181. {
  182.   [modifyMenuItem setEnabled:flag];
  183. }
  184.  
  185. - (void)enableDelete:(BOOL)flag
  186. {
  187.   [deleteButton setEnabled:flag];
  188. }
  189.  
  190. - (void)enableUndelete:(BOOL)flag
  191. {
  192.   [undeleteButton setEnabled:flag];
  193. }
  194.  
  195. /*
  196.  * Redisplay from the data in the clientList. Try to re-select the
  197.  * same item afterwards.
  198.  */
  199. - (void)decacheBrowser
  200. {
  201.   /*
  202.    * Find the possibly new position in the list of the selected client,
  203.    * BEFORE redisplaying the browser from the list...
  204.    */
  205.   int row = [clientList indexOf:[self selectedClient]];
  206.  
  207.   [browser loadColumnZero];
  208.   [[browser matrixInColumn:0] selectCellAt:row :0];
  209.   [self checkStartButton];
  210. }
  211.  
  212. - (NXTypedStream *)openArchive:(int)mode
  213. {
  214.   NXTypedStream *stream ;  
  215.  
  216.   if ( (stream = NXOpenTypedStreamForFile( filename, mode )) == NULL ) {
  217.     NXRunAlertPanel( [NXApp appName], "Unable to open client data file: `%s'",
  218.             "Create it when needed", NULL, NULL, filename );
  219.     return nil;
  220.   }
  221.   
  222.   return stream;
  223. }
  224.  
  225. /*
  226.  * Read in the client info from the typestream file
  227.  */
  228. - (int)loadClientInfo
  229. {
  230.   NXTypedStream *stream ;
  231.  
  232.   if ( (stream = [self openArchive:NX_READONLY]) == nil )
  233.     return 0;
  234.  
  235.   NXReadType( stream, "i", &FileVersion );
  236.   [clientList read:stream];
  237.   [clientList sort];
  238.   NXCloseTypedStream(stream) ;
  239.  
  240.   return 1;
  241. }
  242.  
  243. - (int)saveClientInfoToStream:(NXTypedStream *)stream
  244. {
  245.   int version = VERSION;
  246.   
  247.   NXWriteType( stream, "i", &version );
  248.   [clientList write:stream];
  249.   return 1;
  250. }
  251.  
  252. - (int)saveClientInfo
  253. {
  254.   NXTypedStream *stream ;
  255.   char backup[FILENAME_MAX + 1];
  256.  
  257.   /*
  258.    * If this is the first write, move the old filename to
  259.    * filename~ to serve as a backup.
  260.    */
  261.   if ( didBackup == NO ) {
  262.     sprintf( backup, "%s~", filename );
  263.     rename( filename, backup );
  264.     didBackup = YES;
  265.   }
  266.  
  267.   if ( (stream = [self openArchive:NX_WRITEONLY]) == nil )
  268.     return 0;
  269.  
  270.   [self saveClientInfoToStream:stream];
  271.   NXCloseTypedStream(stream);
  272.   return 1;
  273. }
  274.  
  275. /*
  276.  * Edit the selected invoicing template by messaging to the
  277.  * workspace to open the corresponding file. Sender is the
  278.  * Matrix containing the menu of template names.
  279.  */
  280. - editTemplate:sender
  281. {
  282.   id cell = [sender cellAt:[sender selectedRow] :0];
  283.  
  284.   [self initInvoice];
  285.   [invoice editTemplate:[cell title]];
  286.   return self;
  287. }
  288.  
  289. - preferences:sender
  290. {
  291.   [preferences display];
  292.   return self;
  293. }
  294.  
  295. - saveAs:sender
  296. {
  297.   SavePanel *savePanel = [SavePanel new];
  298.   NXTypedStream *stream;
  299.   const char *path;
  300.  
  301.   if ( [savePanel runModalForDirectory:dirname file:""] == 0 )
  302.     return nil;
  303.  
  304.   path = [savePanel filename];
  305.  
  306.   if ( (stream = NXOpenTypedStreamForFile( path, NX_WRITEONLY )) == NULL ) {
  307.     NXRunAlertPanel( [NXApp appName], "Unable to open file for writing: `%s'",
  308.             "What the...?", NULL, NULL, path );
  309.     return nil;
  310.   }
  311.   
  312.   [self saveClientInfoToStream:stream];
  313.   NXCloseTypedStream(stream);
  314.   
  315.   return self;
  316. }
  317.  
  318. - clientList
  319. {
  320.   return clientList;
  321. }
  322.  
  323. - appDidInit:sender
  324. {
  325.   NXRect rect = {{0.0, 0.0}, {64.0, 64.0}};
  326.  
  327.   if ( createPath( dirname, DIRMODE ) != PathCreationOk ) {
  328.     NXRunAlertPanel( [NXApp appName], "Cannot create path `%s'",
  329.             "Damned UNIX!", NULL, NULL, dirname );
  330.     [NXApp terminate:sender];
  331.   }
  332.  
  333.   preferences = [Preferences new];
  334.  
  335.   [self loadClientInfo];
  336.   [self decacheBrowser];
  337.  
  338.   if ( [preferences hideOnAutoLaunch] )
  339.     [NXApp hide:self];
  340.   else
  341.     [window makeKeyAndOrderFront:self];
  342.  
  343.   /* make view that tracks elapsedTime be the appIcon window's contentView */
  344.   appIconView = [[AppIconView alloc] initFrame:&rect 
  345.                    sourceView:elapsedTimeField];
  346.   [[[NXApp appIcon] setContentView:appIconView] free];
  347.  
  348.   [browser setDoubleAction:@selector(inspect:)];
  349.   [browser setTarget:self];
  350.  
  351.   /* If there are no clients defined yet, disable the start buttons */
  352.   [self checkStartButton];
  353.  
  354.   return self;
  355. }
  356.  
  357. /*
  358.  * If we logout, or there's a powerOff,  make sure the time gets saved.
  359.  */
  360. - app:sender powerOffIn:(int)ms andSave:(int)aFlag
  361. {
  362.   return [self appWillTerminate:sender];
  363. }
  364.  
  365. - appDidUnhide:sender
  366. {
  367.   [window makeKeyAndOrderFront:self];
  368.   return self;
  369. }
  370.  
  371. - appWillTerminate:sender
  372. {
  373.   if ( teNum )
  374.     [self stopClock];
  375.   return self;
  376. }
  377.  
  378. - init
  379. {
  380.   char path[FILENAME_MAX + 1];
  381.  
  382.   [super init];
  383.  
  384.   stopwatch = [[StopWatch alloc] init];
  385.  
  386.   clientList = [[SortList alloc] init];
  387.   [clientList setAutoSort:YES];
  388.   [clientList setDelegate:self];
  389.  
  390.   sprintf( path, "%s/Library/%s", NXHomeDirectory(), [NXApp appName] );
  391.   dirname = NXCopyStringBuffer(path);
  392.  
  393.   sprintf( path, "%s/%s", dirname, ARCHIVE_FILE );
  394.   filename = NXCopyStringBuffer(path);
  395.  
  396.   return self;
  397. }
  398.  
  399. - (const char *) description
  400. {
  401.   return [description stringValue];
  402. }
  403.  
  404. /*
  405.  * Called once per minute by the timed entry routine while the clock is running.
  406.  */
  407. - showElapsedTime
  408. {
  409.   [elapsedTimeField setStringValue:[stopwatch elapsedTime]];
  410.   [appIconView display];
  411.   return self;
  412. }
  413.  
  414. /*
  415.  * Respond to the user's selection of a client
  416.  */
  417. - selectClient:sender
  418. {
  419.   /* Assume that this means we should stop the previous client */
  420.   if ( [stopwatch running] == YES )
  421.     [startButton performClick:sender];
  422.  
  423.   activeClient = [self selectedClient];
  424.   [description setStringValue:[activeClient lastDescription]];
  425.   [description selectText:sender];
  426.   return self;
  427. }
  428.  
  429. /*
  430.  * The start button highlights, but we need to force the title to "Stop".
  431.  * Setting the Alternate Title didn't seem to do the right thing in IB.
  432.  */
  433. - startClock
  434. {
  435.   id font = [elapsedTimeField font];
  436.  
  437.   [elapsedTimeField setFont:[[FontManager new] convertWeight:YES of:font]];
  438.   [self addTimedEntry];
  439.   [startButton   setTitle:"Stop"];
  440.   [startMenuItem setTitle:"Stop"];
  441.   [stopwatch startWatch];
  442.   [self showElapsedTime];
  443.   activeClient = [self selectedClient];
  444.   return self;
  445. }
  446.  
  447. /*
  448.  * The mirror image of the above routine
  449.  */
  450. - stopClock
  451. {
  452.   id font = [elapsedTimeField font];
  453.   [elapsedTimeField setFont:[[FontManager new] convertWeight:NO of:font]];
  454.   [self removeTimedEntry];
  455.   [startButton   setTitle:"Start"];
  456.   [startMenuItem setTitle:"Start"];
  457.   [stopwatch stopWatch];
  458.   [self showElapsedTime];
  459.   [self addSession:[stopwatch startDateString]
  460.            time:[stopwatch startTimeString]
  461.       duration:[stopwatch elapsedMinutes]
  462.        description:[self description]];
  463.   activeClient = nil;
  464.   return self;
  465. }
  466.  
  467. /*
  468.  * Called whenever the startButton is pressed.
  469.  */
  470. - buttonHandler:sender
  471. {
  472.   if ( [startButton state] == 1 )
  473.     [self startClock];
  474.   else
  475.     [self stopClock];
  476.  
  477.   return self;
  478. }
  479.  
  480. - showInfo:sender
  481. {
  482.   [[InfoPanel new] showInfo];
  483.   return self;
  484. }
  485.  
  486. /*
  487.  * Inspect the currently selected client
  488.  */
  489. - inspect:sender
  490. {
  491.   Matrix *matrix = [browser matrixInColumn:0];
  492.   ClientInspector *inspector = [ClientInspector sharedInstance];
  493.  
  494.   [inspector selectClientAt:[matrix selectedRow]];
  495.   [inspector display];
  496.   return self;
  497. }
  498.  
  499. - inspectSessions:sender
  500. {
  501.   [[ClientInspector sharedInstance] showHours:sender];
  502.   return self;
  503. }
  504.  
  505. - inspectExpenses:sender
  506. {
  507.   [[ClientInspector sharedInstance] showExpenses:sender];
  508.   return self;
  509. }
  510.  
  511. - inspectClients:sender
  512. {
  513.   [[ClientInspector sharedInstance] showClient:sender];
  514.   return self;
  515. }
  516.  
  517. - generateDetail:sender
  518. {
  519.   [self initInvoice];
  520.   [invoice generate:clientList];
  521.   return self;
  522. }
  523.  
  524. /*
  525.  * Find a client by short name
  526.  */
  527. - (ClientInfo *)findClient:(const char *)name
  528. {
  529.   int i, count = [clientList count];
  530.  
  531.   for ( i = 0; i < count; i++ ) {
  532.     ClientInfo *info;
  533.  
  534.     info = [clientList objectAt:i];
  535.     if ( strcmp( name, [info shortName] ) == 0 )
  536.       return info ;
  537.   }
  538.   return nil;
  539. }
  540.  
  541. /*
  542.  * Compact consecutive sessions with identical descriptions into
  543.  * a single session with the same total time.
  544.  */
  545. - compactClients:sender
  546. {
  547.   int i, count = [clientList count];
  548.  
  549.   for ( i = 0; i < count; i++ )
  550.     [[clientList objectAt:i] compactSessions];
  551.  
  552.   [[ClientInspector sharedInstance] display];
  553.  
  554.   [self saveClientInfo];
  555.   return self;
  556. }
  557.  
  558. /*
  559.  * This needs to be cleaned up...
  560.  */
  561. - import:sender
  562. {
  563.   FILE *fp;
  564.   const char *pathname;
  565.   char buf[512], *tok;
  566.   char shortName[80], startDate[80], startTime[80], minutes[80], desc[256];
  567.   id openPanel = [OpenPanel new];
  568.   ClientInspector *inspector = [ClientInspector sharedInstance];
  569.   char delimiter[2], endDelimiters[10];
  570.  
  571.   if ( [openPanel runModal] == 0 )
  572.     return nil;
  573.  
  574.   pathname = [openPanel filename];
  575.  
  576.   if ( ! (fp = fopen( pathname, "r" ) ) ) {
  577.     NXRunAlertPanel( [NXApp appName], "Unable to open import file: `%s'",
  578.             "Eat me!", NULL, NULL, pathname );
  579.     return self;
  580.   }
  581.   
  582.   sprintf( delimiter,     "%c",   DELIMITER );
  583.   sprintf( endDelimiters, "%c\n", DELIMITER );
  584.  
  585.   while ( fgets(buf, sizeof(buf), fp) ) {
  586.     ClientInfo *info;
  587.     Session *session;
  588.  
  589.     tok = strtok( buf, delimiter );
  590.     strcpy( shortName, tok ) ;
  591.  
  592.     if ( ! (info = [self findClient:shortName]) ) {
  593.       NXRunAlertPanel( [NXApp appName], "Ignoring unknown client: `%s'",
  594.               "Who needs 'em?", NULL, NULL, shortName );
  595.       continue;
  596.     }
  597.  
  598.     tok = strtok( NULL, delimiter );
  599.     strcpy( startDate, tok );
  600.  
  601.     tok = strtok( NULL, delimiter );
  602.     strcpy( startTime, tok );
  603.  
  604.     tok = strtok( NULL, delimiter );
  605.     strcpy( minutes, tok );
  606.  
  607.     tok = strtok( NULL, endDelimiters ); /* throw out newline too.  */
  608.     strcpy( desc, tok );
  609.  
  610.     session = [[Session alloc]
  611.            init:startDate time:startTime
  612.            duration:atoi(minutes) description:desc];
  613.  
  614.     [info addSession:session];
  615.     [inspector updatedInfo:info];
  616.   }
  617.  
  618.   fclose(fp);
  619.   [self saveClientInfo];
  620.   return self;
  621. }
  622.  
  623. - export:sender
  624. {
  625.   FILE *fp;
  626.   const char *pathname;
  627.   int i, count = [clientList count];
  628.   id savePanel = [SavePanel new];
  629.  
  630.   if ( [savePanel runModal] == 0 )
  631.     return nil;
  632.  
  633.   pathname = [savePanel filename];
  634.  
  635.   if ( ! (fp = fopen( pathname, "w" ) ) ) {
  636.     NXRunAlertPanel( [NXApp appName], "Unable to open export file: `%s'",
  637.             "I'll be darned!", NULL, NULL, pathname );
  638.     return self;
  639.   }
  640.   
  641.   for ( i = 0; i < count; i++ )
  642.     [[clientList objectAt:i] exportToFile:fp];
  643.  
  644.   fclose(fp);
  645.   return self;
  646. }
  647.  
  648. /*
  649.  * Clear out all session information from all clients.
  650.  */
  651. - closeMonth:sender
  652. {
  653.   ClientInspector *inspector = [ClientInspector sharedInstance];
  654.  
  655.   /* Give the user a chance to change their mind... */
  656.   if ( NXRunAlertPanel( [NXApp appName], "Delete all session and expense data?",
  657.            "Delete all data", "Hell no!", NULL ) == NX_ALERTDEFAULT ) {
  658.     [clientList makeObjectsPerform:@selector(deleteSessionsAndExpenses)];
  659.     [inspector closeMonth];
  660.     [self saveClientInfo];    /* write the newly empty file */
  661.     [inspector display];
  662.   }
  663.  
  664.   return self;
  665. }
  666.  
  667. @implementation Controller(PRIVATE)
  668.  
  669. - initInvoice
  670. {
  671.   char path[FILENAME_MAX + 1];
  672.  
  673.   if ( invoice == nil ) {
  674.     sprintf( path, "%s/%s", dirname, TEMPLATE_DIR );
  675.     invoice = [[Invoice alloc] initTemplateDir:path];
  676.   }
  677.   return invoice;
  678. }
  679.  
  680. - (int)selectedRow
  681. {
  682.   return [[browser matrixInColumn:0] selectedRow];
  683. }
  684.  
  685. - selectedClient
  686. {
  687.   return [clientList objectAt:[self selectedRow]];
  688. }
  689.  
  690. /*
  691.  * Make sure the start buttons are disabled if there are
  692.  * no clients defined, and enabled if there are.
  693.  */
  694. - (void)checkStartButton
  695. {
  696.   BOOL flag = ( [clientList count] ? YES : NO );
  697.  
  698.   [startMenuItem setEnabled:flag];
  699.   [startButton   setEnabled:flag];
  700.  
  701.   /* the same goes for these */
  702.   [sessionMenuItem setEnabled:flag];
  703.   [expenseMenuItem setEnabled:flag];
  704.  
  705.   /*
  706.    * The only reasonable thing to do if there are no
  707.    * clients is to create some!
  708.    */
  709.   if ( flag == NO )
  710.     [self inspectClients:nil];
  711. }
  712.  
  713. /*
  714.  * Create a new session object and add it to the proper client's
  715.  * ClientInfo list. Tell the browser what happened so it can update.
  716.  */
  717. - (void)addSession:(const char *)startDate
  718.           time:(const char *)startTime
  719.       duration:(int)minutes
  720.        description:(const char *)desc
  721. {
  722.   Session *session = [[Session alloc]
  723.                 init:startDate time:startTime
  724.                    duration:minutes description:desc];
  725.   [activeClient addSession:session];
  726.   [[ClientInspector sharedInstance] updatedInfo:activeClient];
  727.   [self saveClientInfo];
  728. }
  729.  
  730. /*
  731.  * Compare two ClientInfo objects. Sort alpha by long name.
  732.  */
  733. - (int)compare:obj1 :obj2
  734. {
  735.   return strcmp( [obj1 clientName], [obj2 clientName] );
  736. }
  737.  
  738. /*
  739.  * Delegated method of NXBrowser.  This should be consolidated into a single
  740.  * object.  Right now this method appears (almost) identically in the Controller
  741.  * and the ClientMgr...  (Here we use the shortName instead of the full one.)
  742.  */
  743. - (int) browser:sender fillMatrix:matrix inColumn:(int)column
  744. {
  745.   int i, count = [clientList count];
  746.   
  747.   for ( i = 0; i < count; i++ ) {
  748.     const char *name;
  749.     id cell;
  750.     
  751.     [matrix addRow];
  752.     name = [[clientList objectAt:i] shortName];
  753.     cell = [matrix cellAt:i :0];    /* 1 dimen. matrix: always use col 0! */
  754.     [cell setStringValue:name];
  755.     [cell setLoaded:YES];
  756.     [cell setLeaf:YES];
  757.   }
  758.  
  759.   return count ;
  760. }
  761.  
  762. @end
  763.